

type AssetType = cc.Asset | cc.Asset[];
type DownLoadOption = {
    priority?: number;
    maxRetryCount?: number;
    maxConcurrency?: number;
    force?: boolean;
    cacheEnabled?: boolean;
};

/**
 * 资源加载类
 * 动态加载的资源会自动加引用计数统计
 * 推荐在资源不用的时候使用 decRef 移除引用计数，并将资源引用设为 null
 * 也可以手动强制释放资源
 */
export class ResLoadMgr {
    private static _instance: ResLoadMgr;
    public static get Instance(): ResLoadMgr {
        if (!ResLoadMgr._instance) {
            ResLoadMgr._instance = new ResLoadMgr();
            // 注册cocos默认的resources包
            ResLoadMgr._instance._bundleCache.set('resources', cc.resources);
        }
        return ResLoadMgr._instance;
    }

    private assets: {
        [path: string]: cc.Asset;
    } = Object.create(null);

    private _bundleCache: Map<string, cc.AssetManager.Bundle> = new Map<string, cc.AssetManager.Bundle>();

    /**
     * 清空数据或初始化
     */
    clear(): void {
        this.relaseAllResource();
    }

    /**
     * 加载bundle(默认resources）相对路径文下的资源
     * @param path 本地资源路径
     */

    loadRes<T extends cc.Asset>(path: string, type?: typeof cc.Asset, bundle?: cc.AssetManager.Bundle): Promise<T>;
    loadRes<T extends cc.Asset>(path: string[], type?: typeof cc.Asset, bundle?: cc.AssetManager.Bundle): Promise<T[]>;

    loadRes<T extends cc.Asset>(path: string, type?: typeof cc.Asset): Promise<T>;
    loadRes<T extends cc.Asset>(path: string[], type?: typeof cc.Asset): Promise<T[]>;

    loadRes(path: string | string[], type = cc.Asset, bundle = cc.resources): Promise<AssetType> {
        return new Promise((resolve, reject) => {
            bundle.load(path as any, type, (err: Error, res: AssetType) => {
                if (err) {
                    reject(err);
                    return;
                }
                // 使用引用计数管理
                // 参考 https://docs.cocos.com/creator/manual/zh/scripting/dynamic-load-resources.html#%E8%B5%84%E6%BA%90%E7%9A%84%E4%BE%9D%E8%B5%96%E5%92%8C%E9%87%8A%E6%94%BE
                if (res instanceof cc.Asset) {
                    this.assets[path as string] = res;
                    res.addRef();
                } else {
                    res.forEach((r, i) => {
                        this.assets[path[i]] = r;
                        r.addRef();
                    });
                }

                resolve(res);
            });
        });
    }

    loadDir<T extends cc.Asset>(path: string, type = cc.Asset): Promise<T[]> {
        return new Promise((resolve, reject) => {
            cc.resources.loadDir(path, type, (err, assets: T[]) => {
                if (err) {
                    reject(err);
                    return;
                }
                assets.forEach(r => {
                    r.addRef();
                    this.assets[`${path}/${r.name}`] = r;
                });
                resolve(assets);
            });
        });
    }

    /**
     * 此方法可以加载多个资源，并且不会因为某个资源报错终止加载其他资源
     * 如果加载失败，对应资源的位置为 null
     * @param pathArray 资源路径数组
     * @param type 资源类型
     * @returns
     */
    loadMultiRes(pathArray: string[], type = cc.Asset): Promise<cc.Asset[]> {
        return new Promise(resolve => {
            const assets = new Map<number, cc.Asset>();
            pathArray.forEach((path, i) => {
                cc.resources.load(path, type, (err, asset: cc.Asset) => {
                    if (err) {
                        assets.set(i, null);
                    } else {
                        assets.set(i, asset);
                        this.assets[path] = asset;
                        asset.addRef();
                    }
                    if (assets.size >= pathArray.length) {
                        resolve(Array.from(assets.values()));
                    }
                });
            });
        });
    }

    /**
     * 加载远程路径、绝对路径或者相对于搜索路径的资源
     * @param url
     * @param options
     * @returns
     */
    loadRemote(url: string, options: DownLoadOption = {}): Promise<cc.Asset> {
        return new Promise((resolve, reject) => {
            if (this.assets[url]) {
                resolve(this.assets[url]);
                return;
            }
            cc.assetManager.loadRemote(url, options, (err: Error, res: cc.Asset) => {
                if (err) {
                    reject(err);
                    return;
                }

                this.assets[url] = res;
                res.addRef();
                resolve(res);
            });
        });
    }

    async loadBundle(bundleName='resources') {
        if (this._bundleCache.has(bundleName)) return this._bundleCache.get(bundleName);
        return new Promise((resolve, reject) => {
            cc.assetManager.loadBundle(this.GetBundleUrl(bundleName), (err, bundle: cc.AssetManager.Bundle) => {
                if (err) {
                    reject(err);
                    return;
                }
                this._bundleCache.set(bundle.name, bundle);
                resolve(bundle);
            });
        });
    }

    public loadBundleRes<T extends cc.Asset>(bundleName: string, path: string, type?: typeof cc.Asset): Promise<T>;
    public loadBundleRes<T extends cc.Asset>(bundleName: string, path: string[], type?: typeof cc.Asset): Promise<T[]>;
    public loadBundleRes(bundleName: string, path: string | string[], type?: typeof cc.Asset): Promise<AssetType> {
        return new Promise((resolve, reject) => {
            const bundle = this.getBundle(bundleName);
            if (!bundle) {
                const msg = `bundleConfig中没有${bundleName}`;
                reject(new Error(msg));
                return;
            }
            bundle.load(path as any, type, (err: Error, res: AssetType) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(res);
            });
        });
    }

    public preLoadBundleRes(bundleName: string, path: string, type?: typeof cc.Asset, onProgress?): Promise<boolean>;
    public preLoadBundleRes(bundleName: string, path: string[], type?: typeof cc.Asset, onProgress?): Promise<boolean>;
    public preLoadBundleRes(bundleName: string, path: string | string[], type?: typeof cc.Asset, onProgress?): Promise<boolean> {
        return new Promise((resolve, reject) => {
            const bundle = this.getBundle(bundleName);
            if (!bundle) {
                const msg = `bundleConfig中没有${bundleName}`;
                reject(new Error(msg));
                return;
            }
            bundle.preload(path as any, type, onProgress, (err, res) => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve(true);
            });
        });
    }

    public GetBundleUrl(bundleName='resources') {
        return bundleName;
    }

    /**
     * 
     * @param bundleName 	preloadDir(dir: string, type: typeof cc.Asset, onProgress: (finish: number, total: number, item: RequestItem) => void, onComplete: (error: Error, items: RequestItem[]) => void): void;
        preloadDir(dir: string, onProgress: (finish: number, total: number, item: RequestItem) => void, onComplete: (error: Error, items: RequestItem[]) => void): void;
        preloadDir(dir: string, type: typeof cc.Asset, onComplete: (error: Error, items: RequestItem[]) => void): void;
        preloadDir(dir: string, type: typeof cc.Asset): void;
        preloadDir(dir: string, onComplete: (error: Error, items: RequestItem[]) => void): void;
        preloadDir(dir: string): void;	
     * @param path 
     * @param type 
     */

    public preLoadBundleDir(
        bundleName: string,
        path: string,
        type?: typeof cc.Asset,
        onProgress?: (finish: number, total: number, item: cc.AssetManager.RequestItem) => void,
        onComplete?: (error: Error, items: cc.AssetManager.RequestItem[]) => void
    ): void {
        const bundle = this.getBundle(bundleName);
        if (!bundle) {
            const msg = `bundleConfig中没有${bundleName}`;
            console.log(msg);
            return;
        }
        bundle.preloadDir(path, type, onProgress, onComplete);
    }

    public getBundle(bundleName: string): cc.AssetManager.Bundle {
        const bundle = this._bundleCache.get(bundleName);
        if (!bundle) {
            return null;
        }
        return bundle;
    }

    public removeBundle(bundleName: string): void {
        const bundle = this._bundleCache.get(bundleName);
        if (!bundle) return;
        bundle.releaseAll();
        cc.assetManager.removeBundle(bundle);
        this._bundleCache.delete(bundleName);
    }

    /**
     * 卸载除resources外的所有bundle,慎用
     */
    public removeAllBundle(): void {
        this._bundleCache.forEach((bundle, name) => {
            // 不卸载resources
            if (name === 'resources') return;
            bundle.releaseAll();
            cc.assetManager.removeBundle(bundle);
        });

        this._bundleCache.clear();
        this._bundleCache.set('resources', cc.resources);
    }

    /**
     * 规范资源的加载释放
     * 提供减少资源的引用计数方法
     * @param path
     * @returns
     */
    decResRef(arg: string | cc.Asset | string[] | cc.Asset[]): void {
        let asset = null;
        let path = '';
        let inputs: (string | cc.Asset)[] = null;
        if (!Array.isArray(arg)) {
            inputs = [arg];
        } else {
            inputs = arg;
        }

        inputs.forEach(input => {
            if (typeof input === 'string') {
                path = input;
                asset = this.assets[path];
            } else {
                Object.entries(this.assets).forEach(([p, res]) => {
                    if (res === arg) {
                        path = p;
                        asset = res;
                    }
                });
            }

            if (!asset) {
                return;
            }

            asset.decRef();
            if (asset.refCount <= 0) {
                delete this.assets[path];
            }
        });
    }

    /**
     * 手动释放所有资源
     */
    private relaseAllResource(): void {
        Object.keys(this.assets).forEach(path => {
            this.releaseResByPath(path);
        });
    }

    /**
     * 手动释放单一资源
     * @param path 资源路径
     * @returns
     */
    private releaseResByPath(path: string | string[]): void {
        if (typeof path === 'string') {
            path = [path];
        }

        path.forEach(p => {
            const asset: cc.Asset = this.assets[p];
            if (!asset) {
                return;
            }

            delete this.assets[p];
            cc.assetManager.releaseAsset(asset);
        });
    }

    /**
     * 强制垃圾收集，只在jsb环境下起效
     */
    forceGarbageCollect(): void {
        cc.sys.garbageCollect();
    }
}
